package edu.kit.aifb.ldsbench.generator.opt.util2;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.Semaphore;
import java.util.logging.Level;

import org.openrdf.model.URI;
import org.openrdf.model.impl.URIImpl;
import org.openrdf.repository.RepositoryException;

import edu.kit.aifb.ldsbench.cli2.CLI;
import edu.kit.aifb.ldsbench.generator.util2.IDistributionValues;
import edu.kit.aifb.ldsbench.generator.util2.Octave;
import edu.kit.aifb.ldstream.learner.util.URIComparator;
import edu.kit.aifb.ldstream.stat.distrib.IDistribution;

public class DoubleInstanceBuffer extends Thread{

	private int bufferSize;
	private int instanceID = 0;
	
	private boolean running;
	private boolean buffer1Available;
	private boolean buffer2Available;

	private Object trigger = new Object();
	private Semaphore rdyBuffers;
	
	private double eventFactor;

	private SesameQuery query;
	private IDistributionValues distribValues;

	private List<Map<URI, List<URI>>> buffer1;
	private List<Map<URI, List<URI>>> buffer2;

	public DoubleInstanceBuffer(int bufferSize) {
		this.bufferSize = bufferSize;
		this.distribValues = new Octave();
		this.rdyBuffers = new Semaphore(0);
		try {
			this.query = new SesameQuery(CLI.REPO.getConnection());
		} catch (RepositoryException e) {
			e.printStackTrace();
		}
	}

	public void run(){
		this.running = true;

		while(running){
			// there is an empty buffer, fill it
			fillBuffer();

			if(this.rdyBuffers.availablePermits() == 2){
				// all buffers are available nothing to do
				waitForTrigger();
			}
		}
		shutdownInternal();
	}

	private void fillBuffer(){
		// this method should only be called if there is an empty buffer, but check for safety reasons
		if(!this.buffer1Available){
			this.buffer1 = newBuffer();
			this.buffer1Available = true;
			this.rdyBuffers.release();
		}
		if(!this.buffer2Available){
			this.buffer2 = newBuffer();
			this.buffer2Available = true;
			this.rdyBuffers.release();
		}
	}

	private List<Map<URI, List<URI>>> newBuffer(){
		List<Map<URI, List<URI>>> newBuffer = new ArrayList<Map<URI,List<URI>>>(this.bufferSize);

		/*We want to form a buffer of the form: 
		 * List--------->Map------->List
		 * 
		 *  [0]--------->[type0]---|----[instance0] 
		 *   |              |      |----[instance1]
		 *   |              |      |----[...]
		 *   |              |
		 *   |           [type1]---|----[...]
		 *   |
		 *  [1]--------->[...]
		 *   |
		 * [...]
		 */
		Map<URI, List<URI>> newMap;
		Set<URI> typeSet;
		List<URI> instances;
		for(int i = 0; i < this.bufferSize; i++){
			//create the map
			newMap = new TreeMap<URI, List<URI>>(new URIComparator());
			typeSet = this.query.getTypes();
			for(URI type : typeSet){
				//fill the map
				instances = getInstances(type);
				newMap.put(type, instances);
			}
			newBuffer.add(newMap);
		}
		return newBuffer;
	}


	
	private List<URI> getInstances(URI type){
		List<URI> instances = new LinkedList<URI>();
		IDistribution instanceDistrib = this.query.getDistribution(type, type, SesameQuery.INSTANCE_DISTRIB);
		final int count = (int) (Math.ceil(this.distribValues.getDistributionValue(instanceDistrib) * this.eventFactor));
		if(CLI.log.isLoggable(Level.FINE)) CLI.log.fine("Creating " + count + " instances for type " + type.stringValue());
		//only fill if needed
		if(count > 0){
			final int namesLeft = this.query.getNumInstanceNames(type);
			//find out how much names we need from the repo
			if(namesLeft > 0){
				int num = Math.min(count, namesLeft);
				instances.addAll(this.query.getInstanceNames(type, namesLeft - 1, namesLeft - num));
			}
			// fill the outstanding instance names
			int outstanding = count - namesLeft;
			if(outstanding > 0){
				for(int i = 0; i < outstanding; i++){
					instances.add(buildInstanceName(type, Math.abs(this.instanceID++)));
				}
			}
		}
		return instances;
	}

	private URI buildInstanceName(URI type, int id){
		StringBuilder name = new StringBuilder("http:/").append(type.getLocalName()).append("/instance").append(id);
		return new URIImpl(name.toString());
	}

	public List<Map<URI, List<URI>>> getBuffer(double ef){
		this.eventFactor = ef;
		List<Map<URI, List<URI>>> buffer = null;
		try {
			if(CLI.log.isLoggable(Level.FINE)){
				if(this.rdyBuffers.availablePermits() == 0){
					CLI.log.fine("Run out of buffers!");
				}
			}
			this.rdyBuffers.acquire();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		if(CLI.log.isLoggable(Level.FINE)) CLI.log.fine("Take a ready buffer.");
		//there is a filled buffer
		if(this.buffer1Available){
			// this execution order is important, otherwise the buffer could be replaced 
			buffer = this.buffer1;
			this.buffer1Available = false;
		}else if(this.buffer2Available){
			buffer = this.buffer2;
			this.buffer2Available = false;
		}
		synchronized(this.trigger){
			//trigger to refill the buffer
			this.trigger.notify();
		}
		return buffer;
	}

	private void waitForTrigger(){
		if(CLI.log.isLoggable(Level.FINE)) CLI.log.fine("All buffers are filled.");
		synchronized(this.trigger){
			try {
				this.trigger.wait();
			} catch (InterruptedException e) {
				if(CLI.log.isLoggable(Level.FINE)) CLI.log.log(Level.FINE, "trigger interupted");
			}
		}
	}

	public void shutdown(){
		if(CLI.log.isLoggable(Level.FINE)) CLI.log.fine("Shutting down the DoubleInstanceBuffer");
		this.running = false;
		try {
			//waiting for the buffer to finish
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		synchronized(this.trigger){
			//triggering the buffer, if it is waiting
			this.trigger.notify();
		}
	}


	private void shutdownInternal(){
		if(CLI.log.isLoggable(Level.FINE)) CLI.log.fine("Properly shut down the DoubleInstanceBuffer");
		this.buffer1 = null;
		this.buffer1Available = false;

		this.buffer2 = null;
		this.buffer2Available = false;

		//releasing the waiting thread
		this.rdyBuffers.drainPermits();
		this.rdyBuffers.release();

		this.distribValues.shutdown();
		this.query.close();
		if(CLI.log.isLoggable(Level.FINE)) CLI.log.fine("The DoubleInstanceBuffer has been shut down");
	}
}
